# @Time: 2021/2/20 18:40
# @Author: CLK
# @SoftWare: PyCharm
# -*- coding: utf-8 -*-

"""
哔哩哔哩视频爬取3.0
"""
import os
import time
import re
from enum import Enum
import json
import requests
import my_fake_useragent as useragent
from moviepy.editor import *

download_flag = True

headers = {
    'origin': 'https://www.bilibili.com/',
    'User-agent': useragent.UserAgent().random()
}


class Mode(Enum):
    """
    下载模式枚举类
        MIX：音视频下载，并合成
        AUDIO：只下载音频
        VIDEO：只下载视频
    """

    AUDIO = 0,
    VIDEO = 1,
    MIX = 2


def get_desktop_path():
    return os.path.join(os.path.expanduser("~"), 'Desktop')


def remove_temp(video_name, audio_name):
    """
    删除临时文件

    :return:
    """
    if os.path.exists(video_name):
        os.remove(video_name)
    if os.path.exists(audio_name):
        os.remove(audio_name)


def merge(path, video_name='v.mp4', audio_name='a.mp3', num='v_a'):
    """
    合并视频和音频

    :param video_name: 视频名称
    :param audio_name: 音频名称
    :param path: 合并后存放的本地地址
    :param num: 资源名称
    :return:
    """

    try:
        if not os.path.exists(path):
            os.makedirs(path)
        print()
        if os.path.exists(video_name) and os.path.exists(audio_name):
            save_path = path + f'/{num}.mp4'
            print("正在将音频和视频合并，位置：", save_path)

            video_temp = VideoFileClip(video_name)
            audio_temp = AudioFileClip(audio_name)
            v_a = video_temp.set_audio(audio_temp)
            v_a.write_videofile(save_path, threads=5)

            remove_temp(video_name, audio_name)
            print('合并完成，删除临时文件...\n')
        else:
            print('错误，未找到下载的资源：', video_name, " or ", audio_name)
            exit()
    except KeyError:
        print('链接错误，请尝试将音频资源链接和视频资源链接调换位置再请求！！！')
        exit()


def use_time(start_time, end_time):
    """
    计算程序运行时间

    :param start_time: 起始时间戳
    :param end_time: 结束时间戳
    :return: 时间间隔
    """
    time_stamp = end_time - start_time
    if time_stamp < 1:
        return str(int(time_stamp)) + "s"

    h = str(int(time_stamp / 3600))
    m = str(int(time_stamp / 60))
    s = str(int(time_stamp % 60))
    _time = ''
    if h != '0':
        _time += h + '小时'
    if m != '60' and m != '0':
        _time += m + '分'
    if s != '0':
        _time += s + '秒'
    return _time


class OriginBiliBiliSpider:
    """
    使用手动的方式下载哔哩哔哩资源，需要提供对应的参数
    """

    def start(self, save_path, download_type: Mode):
        """
        :param save_path: 本地存放地址，默认为桌面
        :param download_type: 下载模式，Model.MIX(混合模式，带音频的视频下载)，Model.VIDEO(仅视频下载)，Model.AUDIO(仅音频下载)
        """

        # 在当前路径下创建一个"url.json"文件来读取程序运行所需的参数
        if os.path.exists('url.json'):
            file = json.load(open('url.json'))
            video_url = file['video_url']
            audio_url = file['audio_url']
            referer = file['referer']

            # print(audio_url)
            # print(video_url)
            # print(referer)

            if audio_url or video_url and referer:
                self.one_download(save_path, video_url, audio_url, referer, download_type)

    @staticmethod
    def request_resource(url, referer):
        """
        请求资源

        :param url: 资源链接
        :param referer: 哔哩哔哩资源请求地址参数
        :return: 资源响应体
        """

        response = requests.options(url, headers=headers)
        print('option_status：', response)

        headers['range'] = 'bytes=0-10'
        headers['referer'] = referer

        response = requests.get(url, headers=headers)
        print('get_status: ', response)
        print('get_header：', response.headers)

        if response.status_code == 403:
            exit()

        resource_count = response.headers.get('content-range').split('/')[1]
        headers['range'] = 'bytes=0-' + resource_count

        print('local_header: ', headers)
        response = requests.get(url, headers=headers)

        return response

    def one_download(self, local_save_path, video_url, audio_url, referer, mode=Mode.MIX):
        """
        哔哩哔哩资源下载

        :param local_save_path: 本地资源存放地址
        :param video_url: 视频资源地址
        :param audio_url: 音频资源地址
        :param referer: 哔哩哔哩资源请求地址参数
        :param mode: 模式
        :return:
        """

        start_time = time.time()

        if local_save_path is None:
            local_save_path = get_desktop_path()

        audio_response = None
        video_response = None
        _file = None
        print('User-agent: ', headers['User-agent'])

        if mode is Mode.AUDIO or mode is Mode.MIX:
            print('开始下载音频。。。')
            audio_response = self.request_resource(audio_url, referer)
            audio_size = len(audio_response.content)
            if audio_size == 0:
                print('音频下载失败！大小：', audio_size)
                exit()
            else:
                print('音频下载成功！大小：', audio_size)

        if audio_response is not None:
            print('音频开始写入。。。')
            if mode is Mode.MIX:
                audio_name = 'a.mp3'
            else:
                audio_name = local_save_path + '/a.mp3'
            _file = open(audio_name, 'wb')
            _file.write(audio_response.content)

            print('音频写入成功！')

        if mode is Mode.VIDEO or mode is Mode.MIX:
            print('开始下载视频。。。')
            video_response = self.request_resource(video_url, referer)
            video_size = len(video_response.content)
            if video_size == 0:
                print('视频下载失败！大小：', video_size)
                exit()
            else:
                print('视频下载成功！大小：', video_size)

        if video_response is not None:
            print('视频开始写入。。。')
            if mode is Mode.MIX:
                video_name = 'v.mp4'
            else:
                video_name = local_save_path + '/v.mp4'
            _file = open(video_name, 'wb')
            _file.write(video_response.content)
            print('视频写入成功！')

        _file.close()

        if mode is Mode.MIX and audio_response is not None and video_response is not None:
            merge(local_save_path)

        end_time = time.time()
        print('耗时：', use_time(start_time, end_time), '秒')


class AutomaticParsingBiliBiliSpider:
    """
    能够根据网址自动解析，来下载资源
    """

    @staticmethod
    def download(_url, source_name):
        """
        指定url下载资源

        :param _url: 目标地址
        :param source_name: 资源名称
        :return:
        """
        if len(_url) == 0:
            print('url为空')
            exit()
        requests.options(_url)
        response = requests.get(_url, headers=headers)
        _count = response.headers['Content-Range'].split('/')[1] + '-1'
        headers['range'] = f'bytes=0-{eval(_count)}'
        response = requests.get(_url, headers=headers)
        _file = open(source_name, 'wb')
        _file.write(response.content)
        _file.close()

    @staticmethod
    def analysis(_url):

        """
        对指定url网页进行解析

        :param _url: 目标地址
        :return: 视频地址，视频资源请求头（range），音频地址，音频资源请求头（range）
        """
        html_text = requests.get(_url, headers={'User-agent': useragent.UserAgent().random()})
        video_url = re.findall('"video":.*?{.*?"base_url":"(.*?)",', html_text.text)[0]
        audio_url = re.findall('"audio":.*?{.*?"base_url":"(.*?)",', html_text.text)[0]
        video_range_initialization = re.findall('"initialization":"(.*?)"', html_text.text)[0]
        audio_range_initialization = re.findall('"audio":.*?{.*?"initialization":"(.*?)"', html_text.text)[0]

        headers['range'] = 'bytes=' + video_range_initialization
        headers['referer'] = _url
        print(headers)
        video_response = requests.get(video_url, headers=headers)

        headers['range'] = 'bytes=' + audio_range_initialization
        headers['referer'] = _url
        print(headers)
        audio_response = requests.get(audio_url, headers=headers)

        if video_response.status_code == 403 or audio_response.status_code == 403:
            print("视频地址解析响应码：", video_response.status_code)
            print("音频地址解析响应码：", audio_response.status_code)
            exit()

        video_range = video_response.headers.get('content-range').split('/')[1]
        audio_range = audio_response.headers.get('content-range').split('/')[1]

        return video_url, '0-' + video_range, audio_url, '0-' + audio_range

    def one_download(self, local_save_path, _url, _type=Mode.MIX):

        """
        单个资源下载

        :param local_save_path: 本地资源存放地址
        :param _url: 资源地址
        :param _type: 下载模式
        :return:
        """

        start_time = time.time()
        print('开始解析网页：', _url)
        video_url, video_range, audio_url, audio_range = self.analysis(_url)

        if _type != Mode.VIDEO:
            print('开始下载音频...')
            headers['range'] = f'bytes={audio_range}'
            if _type == Mode.AUDIO:
                self.download(audio_url, get_desktop_path() + '\\audio.mp3')
            else:
                self.download(audio_url, 'a.mp3')
            print('音频下载成功...')

        if _type != Mode.AUDIO:
            print('开始下载视频...')
            headers['range'] = f'bytes={video_range}'
            if _type == Mode.VIDEO:
                self.download(video_url, get_desktop_path() + '\\video.mp4')
            else:
                self.download(video_url, 'v.mp4')
            print('视频下载成功...')

        if _type == Mode.MIX:
            merge(local_save_path)

        end_time = time.time()
        print(f'资源下载完成，用时：', use_time(start_time, end_time))

    @staticmethod
    def one_specific_download(local_save_path, video_url, audio_url):
        """
        哔哩哔哩特殊资源下载

        :param local_save_path: 本地资源存放地址
        :param video_url: 视频资源地址
        :param audio_url: 音频资源地址
        :return:
        """
        headers['deadline'] = int(time.time()) + 7200

        video_response = requests.options(video_url, headers=headers)
        audio_response = requests.options(audio_url, headers=headers)

        print('video_response_option：', video_response)
        print('audio_response_option：', audio_response)

        video_response = requests.get(video_url, headers=headers)
        audio_response = requests.get(audio_url, headers=headers)

        print('video_response_get：', video_response)
        print('audio_response_get：', audio_response)

        _file = open('v.mp4', 'wb')
        _file.write(video_response.content)

        _file = open('a.mp3', 'wb')
        _file.write(audio_response.content)

        _file.close()

        merge(local_save_path)

    def more_download(self, local_save_path, start, end, _url):
        """
        多个资源下载

        :param local_save_path: 本地资源存放地址
        :param start: 开始集
        :param end: 结束集
        :param _url: 开始集地址
        :return:
        """

        variate = re.findall(r'.*?p=(.?)&?', _url)

        if len(variate) == 0:
            print('链接错误，请检查链接是否符合格式...')
            exit()

        count_start_time = time.time()
        for i in range(start, end + 1):
            index = _url.rindex(variate[0])
            break_url = [_url[0:index], _url[index + 1:]]
            _url = break_url[0] + str(i) + break_url[1]

            print(f'正在下载第{i}集...')
            # 解析网页
            video_url, video_range, audio_url, audio_range = self.analysis(_url)

            # 下载视频
            headers['range'] = f'bytes={video_range}'
            self.download(video_url, f'v_{i}.mp4')

            # 下载音频
            headers['range'] = f'bytes={audio_range}'
            self.download(audio_url, f'a_{i}.mp3')

            merge(local_save_path, f'v_{i}.mp4', f'a_{i}.mp3', str(i))

        count_end_time = time.time()
        print(f'视频全部下载完成，共{end - start + 1}集，总共用时：', use_time(count_start_time, count_end_time))


if __name__ == '__main__':
    # 自动解析资源路径下载资源
    url = "https://www.bilibili.com/video/BV1a64y1q7kR/?spm_id_from=333.337.search-card.all.click&vd_source=cec87be7cbd9b099c371b26c18d778a7"
    bilibili_spider = AutomaticParsingBiliBiliSpider()
    bilibili_spider.one_download(get_desktop_path(), url, Mode.MIX)

    # 手动输入资源路径下载资源
    # bilibili_spider = OriginBiliBiliSpider()
    # bilibili_spider.start(save_path, Model)

    pass
